Why React Re-Renders
So, before we can talk about useMemo
and useCallback
, we need to get really comfortable with the React render cycle.
Throughout this course, we've seen how we can call a state setter function (eg. setCount
, setUser
) to trigger a re-render. React needs to capture a snapshot of what the UI should look like given this new value for a state variable.
In fact, we've already learned a lot about re-renders. In this lesson, we're going to refine what we've learned, to poke at it a bit and see if we can make things even clearer.
The basic rule
So, let's start with a fundamental truth: Every re-render in React starts with a state change. It's the only “trigger” in React for a component to re-render.
Now, that probably doesn't sound right... after all, don't components re-render when their props change?
Here's the thing: when a component re-renders, it also re-renders all of its descendants.
Let's look at an example:
Code Playground
In this example, we have 3 components: App
at the top, which renders Counter
, which renders BigCountNumber
.
In React, every state variable is attached to a particular component instance. In this example, we have a single piece of state, count
, which is associated with the Counter
component.
Whenever this state changes, Counter
re-renders. And because BigCountNumber
is being rendered by Counter
, it will re-render as well.
Here's an interactive graph that shows this mechanic in action. Click the “Increment” button to trigger a state change:
App
Counter
count: 0
BigCountNumber
Props: { count }
(The green flash signifies that a component is re-rendering.)
Alright, let's clear away Big Misconception #1: The entire app re-renders whenever a state variable changes.
I know some developers believe that every state change in React forces an application-wide render, but this isn't true. Re-renders only affect the component that owns the state + its descendants (if any). The App
component, in this example, doesn't have to re-render when the count
state variable changes.
Rather than memorize this as a rule, though, let's take a step back and see if we can figure out why it works this way.
React's “main job” is to keep the application UI in sync with the React state. The point of a re-render is to figure out what needs to change.
Let's consider the “Counter” example above. When the application first mounts, React renders all of our components and comes up with the following sketch for what the DOM should look like:
<main> <p> <span class="prefix">Count:</span> 0 </p> <button> Increment </button></main><footer> <p>Copyright 2022 Big Count Inc.</p></footer>
When the user clicks on the button, the count
state variable flips from 0
to 1
. How does this affect the UI? Well, that's what we hope to learn from doing another render!
React re-runs the code for the Counter
and BigCountNumber
components, and we generate a new picture of the DOM we want:
<main> <p> <span class="prefix">Count:</span> 1 </p> <button> Increment </button></main><footer> <p>Copyright 2022 Big Count Inc.</p></footer>
As we've learned, each render is like a snapshot, a photo that tells us what the UI should look like, based on the current application state.
React then plays a “find the differences” game to figure out what's changed between snapshots. In this case, it sees that our paragraph has a text node that changed from 0
to 1
, and so it edits the text node to match the snapshot. Satisfied that its work is done, React settles back and waits for the next state change. This is what we've called the core React loop.
With this framing in mind, let's look again at our render graph:
App
Counter
count: 0
BigCountNumber
Props: { count }
Our count
state is associated with the Counter
component. Because data can't flow "up" in a React application, we know that this state change can't possibly affect <App />
. And so we don't need to re-render that component.
But we do need to re-render Counter
's child, BigCountNumber
. This is the component that actually displays the count
state. If we don't render it, we won't know that our paragraph's text node should change from 0
to 1
. We need to include this component in our sketch.
The point of a re-render is to figure out how a state change should affect the user interface. And so we need to re-render all potentially-affected components, to get an accurate snapshot.
It's not about the props
Alright, let's talk about Big Misconception #2: A component will re-render because its props change.
Let's explore with an updated example.
In the code below, our “Counter” app has been given a brand new component, Decoration
:
Code Playground
(It was getting a bit crowded, having all of the components in a single big file, so I took the liberty of re-organizing. But the overall component structure is the same, aside from the new Decoration
component.)
Our counter now has a cute lil’ sailboat in the corner, rendered by the Decoration
component. It doesn't depend on count
, so it probably won't re-render when count
changes, right?
Well, er, not quite.
App
Counter
count: 0
BigCountNumber
Props: { count }
Decoration
When a component re-renders, it tries to re-render all descendants, regardless of whether they're being passed a particular state variable through props or not.
Now, this seems counter-intuitive... If we aren't passing count
as a prop to <Decoration>
, why would it need to re-render??
Here's the answer: it's hard for React to know, with 100% certainty, whether <Decoration>
depends, directly or indirectly, on the count
state variable.
In an ideal world, React components would always be “pure”. A pure component is one that always produces the same UI when given the same props.
In the real world, many of our components are impure. It's surprisingly easy to create an impure component. For example:
function CurrentTime() { const now = new Date();
return ( <p>It is currently {now.toString()}</p> );}
This component will display a different value whenever it's rendered, since it relies on the current time!
A sneakier version of this problem has to do with refs. If we pass a ref as a prop, React won't be able to tell whether or not we've mutated it since the last render. And so it chooses to re-render, to be on the safe side.
React's #1 goal is to make sure that the UI that the user sees is kept “in sync” with the application state. And so, React will err on the side of too many renders. It doesn't want to risk showing the user a stale UI.
So, to go back to our misconception: props have nothing to do with re-renders. Our <BigCountNumber>
component isn't re-rendering because the count
prop changed.
When a component re-renders, because one of its state variables has been updated, that re-render will cascade all the way down the tree, in order for React to figure out what the new snapshot should look like.
That said, there are some things we can do to optimize this process. We'll learn about our options in the next few lessons.